前面幾天,截圖、manifest、正文都準備好了,但到目前為止,它們還是一堆散落在 screenshots/ 與 docs/ 裡的檔案。
今天就來做最後一步:把這些檔案素材組裝成 Word 與 PDF,成為可以交付的文件。
先前在 Day 03 定下的硬需求之一就是「要有 Word 檔」 (因為代理商要能改內容、業務要能抽幾頁去做簡報)。所以交付格式的主角是 .docx,PDF 則是從它轉出來的副產品。
另外,一份交付文件至少要有封面、目錄、頁碼,少了這些,內容再好看起來也像一份草稿。
整條路線在 Day 05 已經畫過了:正文跟截圖合流之後交給 pandoc,搭配排版模板產出 Word 與 PDF。範例專案 auto-manual-gen 為此新增了一個指令:
npm run build # output/manual.md → output/manual.docx
npm run build -- --pdf # 再用 Word 更新目錄頁碼、轉出 output/manual.pdf
build 分成兩階段:先合併,再轉換。
pandoc 不認得 {{legend.confirm}},也不知道 {{screenshot:camera-add-01}} 要放哪張圖。所以在交給 pandoc 之前,要先把五章正文依 manifest 的 order 串起來,把這些引用展開,寫成一份 output/manual.md。
以 Day 18 那章「新增攝影機」為例,合併之後長這樣:
# 5 新增攝影機
這一章說明怎麼在 DemoStreamApp 建立一台攝影機,並填入它的 RTSP 位址。
> 重要:攝影機建立並啟用推論後,會持續拍攝並分析畫面中的人員影像。……
## 操作步驟
1. 等待左側的「攝影機清單」載入完成。
2. 點擊清單右上角的「新增攝影機」。
{width=2.52in}
| 標號 | 說明 |
|:---:|:---|
| 1 | 顯示名稱 |
| 2 | 安裝位置 |
| 3 | RTSP 位址 |
| 4 | 啟用推論 |
3. 在「顯示名稱」輸入「大門西側」。
...
合併這一步做了幾件事:
{{legend.name}} 換成 legend 文字
也就是 manifest 裡的「顯示名稱」。Day 17 花了一些力氣把 legend 跟畫面上的字對齊,就是為了這一刻。
{{screenshot:*}} 展開成「圖 + 圖說 + legend 表格」
這是 Day 12 就決定好的呈現方式,圖片下方那張小表格,就是從 manifest 的 annotate 產生的。
拿掉保護區標記
<!-- protected:start --> 是給 validate 看的,讀者不需要看到,但裡面的內容原封不動保留。
章節加上編號
標題變成「5 新增攝影機」,圖號跟著變成「圖 5-1」。
前幾天只有三章有正文,這次順手補上了「介面總覽」與「儲存版面設定」兩章,寫法照
agent/STYLE.md,validate也都通過,這樣產出的才是一本完整的手冊。
另外,合併前會先跑一次跟 validate 相同的正文檢查,並確認每張截圖都拍好了。引用壞掉的正文,不應該被排成一份看起來很正式的文件。
output/manual.md 本身也是很好用的中間產物:交付文件出問題時,先打開它看看,就能分辨是合併出錯,還是 pandoc 或 Word 的排版問題。
截圖的寬度不能全部設成一樣。整個畫面的截圖需要撐滿版面,但像「新增攝影機」對話框這種小元件,如果也撐滿版面,就會被放大到字比正文還大。
這裡的做法是以畫面上的大小為準:截圖是用 deviceScaleFactor: 2 拍的,所以 PNG 寬度除以 2 就是它在畫面上的 CSS px,再用 1 吋 = 96 px 換算成吋。上限則是 A4 扣掉左右 2.5 cm 邊界之後的版心寬,大約 6.3 吋。
所以整頁截圖會是 6.30in,對話框是 2.52in,右下角的 toast 只有一點點大。印出來之後,每張圖裡的文字大小都差不多,這比「每張圖都一樣寬」重要得多。
合併好之後,交給 pandoc:
pandoc output/manual.md -o output/manual.docx \
--reference-doc=templates/reference.docx \
--toc --toc-depth=1 --resource-path=.
pandoc 只負責把 Markdown 的結構(標題、段落、清單、圖片、表格)轉成 Word 的結構。字型、字級、顏色這些長相,全部來自 reference.docx。
reference.docx 是樣式來源,不是模板需要特別說明一下,pandoc 只讀 reference.docx 的樣式,不讀它的內容。就算在裡面寫滿了字,轉出來的文件也不會出現這些字。所以不要想著把封面做在 reference.docx 裡,它不會出現。
(我一開始就是這樣以為的,所以前面的文章是用「模板」這個詞彙,我很抱歉QQ)
做法是先讓 pandoc 吐出它內建的預設樣式檔:
pandoc -o templates/reference.docx --print-default-data-file reference.docx
再用 Word 打開它,修改裡面的樣式後存檔。範例專案調整了這些樣式:
| 樣式 | 調整 | 用在哪裡 |
|---|---|---|
| Normal | 微軟正黑體 11pt、1.3 倍行高 | 所有正文(Body Text、First Paragraph、Compact 都繼承它) |
| Heading 1 | 20pt 粗體、段落前分頁 | 每一章的標題,順便讓每章從新的一頁開始 |
| Heading 2 | 14pt 粗體 | 「操作步驟」「完成後」 |
| Title / Subtitle / Date | 置中、往下推 | 封面 |
| TOC Heading | 段落前分頁 | 讓封面獨立一頁 |
| Captioned Figure / Image Caption | 置中、與下一段同頁 | 圖與圖說,避免圖在這頁、圖說在下一頁 |
| Table | 框線、置中 | legend 表格 |
| Block Text | 左側色條、淺灰底 | 保護區的警語與「注意」 |
版面設定也是在 reference.docx 裡改:A4、左右各 2.5 cm 邊界、頁尾置中頁碼、首頁不同(封面不印頁碼)。pandoc 會沿用 reference.docx 的頁首頁尾與版面設定。
做好的 reference.docx 直接進版控。之後要換一套企業識別,只要換掉這個檔案,一行 Markdown 都不用動。
範例專案把上面這些調整寫成了
tools/style-reference-docx.ps1,用 Word COM 一個一個改樣式,比較好 review,也方便各位重現。實務上直接用 Word 手動改就好。這裡的 COM (Component Object Model) 是 Windows 上讓程式呼叫其他程式功能的機制。Word 透過它開放了整套物件模型,所以可以在 PowerShell 裡用
New-Object -ComObject Word.Application在背景開一個真正的 Word,再用程式開檔、改樣式、存檔,平常在 Word 裡點得到的功能,大多都能這樣呼叫。也因為背後跑的是真的 Word,所以它只能在裝有 Office 的 Windows 上執行。(如果不是 AI Agent,我大概一輩子都不會知道有 COM 這個東西存在XD)
如同前面所說,一份完整的文件需要封面、目錄、頁碼,除了封面之外,這些東西都是 Word 本身都有支援的功能,因此後面兩者會透過 Word 來處理。
這邊會有比較多 Word 的功能細節,就像以前寫碩士論文都需要設定好圖目錄、表目錄、交互參照等,好險現在有 AI Agent 可以幫忙寫腳本自動處理了...
封面上的標題、版本、日期,都是 pandoc 從 Markdown 開頭的 YAML metadata 產生的:
---
title: 'DemoStreamApp 使用手冊'
subtitle: '版本 v1.2.0'
date: '2026-09-25(b17aac5)'
lang: zh-Hant-TW
toc-title: '目錄'
---
這一段是 build 自己寫的,不是人寫的。標題與版本號取自 manifest/manual.yaml,日期與 commit 則取自 git。如果 manifest/ 或 docs/ 有還沒 commit 的變更,日期後面還會多一句「含未提交的變更」,拿到文件的人一眼就知道這份不是正式版本。
封面上的版本號只要有一處是手寫的,它遲早會跟實際內容對不上。這條產線從 Day 01 開始想解決的,就是「文件跟產品對不上」這件事,不能在封面上又製造一個新的來源。
--toc 會讓 pandoc 在文件開頭放一個目錄,但它放的其實只是一個 Word 的欄位 (field)。頁碼要等 Word 真正排過版才算得出來,pandoc 做不到這件事。所以直接打開 manual.docx,目錄會是空的,要按一次 F9 更新才會出現。
要客戶打開文件後自己按 F9,實在不太像話。所以 --pdf 這一步用 Word COM 自動化,把這件事做掉:
$doc = $word.Documents.Open($work)
foreach ($toc in $doc.TablesOfContents) { $toc.Update() } # 更新目錄頁碼
$doc.Save() # 存回 docx
# 17 = wdExportFormatPDF;最後一個參數 1 = 把標題轉成 PDF 書籤
$doc.ExportAsFixedFormat($Pdf, 17, $false, 0, 0, 1, 1, 0, $true, $true, 1)
一次做三件事:更新目錄、把更新好的版本存回 docx、轉出帶書籤的 PDF。所以交出去的 Word 檔,打開就是完整的目錄。
Word 可以自動維護「圖 3-2」這種編號。但這條產線裡,build 本來就知道每一章是第幾章、每張圖是第幾張,直接寫死在圖說裡就好,每次重新產出都會重新算一次。
雖然這樣做的話,之後如果客戶自己在 Word 裡插了一張圖,後面的圖號就不會自動跟著改。不過在這條產線的設計裡,要改內容應該是改 Markdown 再重新產出,這個取捨我覺得可以接受。
範例專案 clone 下來、切到 chore/day20 分支。除了 Node 20+,還需要安裝 pandoc(文章用的是 3.11):
npm install
npm run manual # 先把九張截圖拍好
npm run build # output/manual.md + output/manual.docx
npm run build -- --pdf # 需要 Windows + Word
實際執行的輸出:
$ npm run build -- --pdf
合併 5 章 -> output/manual.md
pandoc -> output/manual.docx
Word -> output/manual.pdf(目錄頁碼已更新)

成品是一份 11 頁的手冊:封面、目錄,以及五章各自從新的一頁開始,每張截圖下方都有圖說與 legend 表格。
檔案我有放在 範例專案的 Release,大家有興趣的話可以下載來看看。
版面還有優化空間,但這就牽涉到一些主觀美感問題,需要慢慢打磨,現在沒時間處理QQ
Word COM 自動化時,Word 是在背景執行的,畫面上什麼都看不到。第一次跑樣式腳本時,腳本中途出錯,finally 裡的 $word.Quit() 被 Word 攔下來問「要不要儲存變更?」,但這個對話框看不見,腳本就永遠卡在那裡。
我手動把 Word 強制結束之後,更麻煩的事來了:之後再用 COM 開同一個路徑,一樣會卡住,但把同一份檔案複製到別的路徑開,一切正常。我猜是 Word 記得「上次這個檔案沒有正常關閉」,開檔時又跳出一個看不見的對話框 (畢竟看不見,只能用猜的 XD)。
最後的做法有三個:
$word.DisplayAlerts = 0,能關掉的提示都關掉。Close 與 Quit 都明確指定「不儲存」,失敗時不會留下改到一半的檔案。如果各位的腳本也卡住,先看看工作管理員裡是不是有好幾個 WINWORD.EXE。
在 Word COM 裡用 $doc.Styles.Item('Heading 1') 取樣式,在中文版 Word 會直接找不到,因為它叫「標題 1」。內建樣式要改用 WdBuiltinStyle 常數取(例如 -2 是 Heading 1),跟介面語言無關。
麻煩的是 Date、TOC Heading 這類樣式沒有對應的常數,只能用名稱找,所以腳本裡只好兩種名稱都試:先找 Date,找不到再找「日期」。pandoc 自己定義的樣式(Image Caption、Captioned Figure)則沒有這個問題,它們在任何語言的 Word 裡都叫這個名字。
lang: zh-TW 會讓 pandoc 一直抱怨一開始 metadata 寫的是 lang: zh-TW,結果每次 build 都會印出一整排警告:
[WARNING] Could not load translations for zh-TW
translations/zh.yaml:
[WARNING] The term Figure has no translation defined.
[WARNING] The term Table has no translation defined.
...
原因是 pandoc 內建的翻譯檔只有 zh-Hant 與 zh-Hans,沒有 zh-TW。改成 lang: zh-Hant-TW 之後就安靜了。這個警告不影響產出 (因為我們沒有用到翻譯),但每張圖、每個表格都印一次,真正重要的警告會被淹沒。
今天把 Markdown 正文跟截圖交付成 Word 與 PDF:
manual.md,再交給 pandoc。reference.docx。manual.yaml 與 git。到這裡,最初的那個「讓 AI Agent 自動建立使用手冊」的目標,終於有了一份實際的成品!
不過,Word 這條路也帶來了對 Windows 與 Office 的依賴。明天來看看,如果不是每次都需要 Word,還有哪些交付方式。(但我暫時用不到就是了)